/*
 * Copyright 2013 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.gl2jni;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.opengl.GLES20;
import android.opengl.GLES30;
import android.opengl.Matrix;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.Spinner;
import android.widget.TextView;
import com.android.gl2jni.encoder.MediaAudioEncoder;
import com.android.gl2jni.encoder.MediaEncoder;
import com.android.gl2jni.encoder.MediaEncoder.MediaEncoderListener;
import com.android.gl2jni.encoder.MediaMuxerWrapper;
import com.android.gl2jni.encoder.TextureMovieEncoder2;
import com.android.gl2jni.encoder.VideoEncoderCore;
import com.android.gl2jni.filter.ASCIIFilterProgram;
import com.android.gl2jni.filter.BarrelBlurFilterProgram;
import com.android.gl2jni.filter.BeautifyFilterProgram;
import com.android.gl2jni.filter.FastBlurFilterProgram;
import com.android.gl2jni.filter.FlatShadedProgram;
import com.android.gl2jni.filter.GlitchFilterProgram;
import com.android.gl2jni.filter.KuwaharaFilterProgram;
import com.android.gl2jni.filter.MoneyFilterProgram;
import com.android.gl2jni.filter.MosaicFilterProgram;
import com.android.gl2jni.filter.ScaleFilterProgram;
import com.android.gl2jni.filter.ShakeFilterProgram;
import com.android.gl2jni.filter.ShineWhiteFilterProgram;
import com.android.gl2jni.filter.SoulOutFilterProgram;
import com.android.gl2jni.filter.TextureFilterProgram;
import com.android.gl2jni.filter.TileMosaicFilterProgram;
import com.android.gl2jni.gles.*;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Locale;
import timber.log.Timber;

/**
 * Demonstrates efficient display + recording of OpenGL rendering using an FBO.  This
 * records only the GL surface (i.e. not the app UI, nav bar, status bar, or alert dialog).
 * <p>
 * This uses a plain SurfaceView, rather than GLSurfaceView, so we have full control
 * over the EGL config and rendering.  When available, we use GLES 3, which allows us
 * to do recording with one extra copy instead of two.
 * <p>
 * We use Choreographer so our animation matches vsync, and a separate rendering
 * thread to keep the heavy lifting off of the UI thread.  Ideally we'd let the render
 * thread receive the Choreographer events directly, but that appears to be creating
 * a permanent JNI global reference to the render thread object, preventing it from
 * being garbage collected (which, in turn, causes the Activity to be retained).  So
 * instead we receive the vsync on the UI thread and forward it.
 * <p>
 * If the rendering is fairly simple, it may be more efficient to just render the scene
 * twice (i.e. configure for display, call draw(), configure for video, call draw()).  If
 * the video being created is at a lower resolution than the display, rendering at the lower
 * resolution may produce better-looking results than a downscaling blit.
 * <p>
 * To reduce the impact of recording on rendering (which is probably a fancy-looking game),
 * we want to perform the recording tasks on a separate thread.  The actual video encoding
 * is performed in a separate process by the hardware H.264 encoder, so feeding input into
 * the encoder requires little effort.  The MediaMuxer step runs on the CPU and performs
 * disk I/O, so we really want to drain the encoder on a separate thread.
 * <p>
 * Some other examples use a pair of EGL contexts, configured to share state.  We don't want
 * to do that here, because GLES3 allows us to improve performance by using glBlitFramebuffer(),
 * and framebuffer objects aren't shared.  So we use a single EGL context for rendering to
 * both the display and the video encoder.
 * <p>
 * It might appear that shifting the rendering for the encoder input to a different thread
 * would be advantageous, but in practice all of the work is done by the GPU, and submitting
 * the requests from different CPU cores isn't going to matter.
 * <p>
 * As always, we have to be careful about sharing state across threads.  By fully configuring
 * the encoder before starting the encoder thread, we ensure that the new thread sees a
 * fully-constructed object.  The encoder object then "lives" in the encoder thread.  The main
 * thread doesn't need to talk to it directly, because all of the input goes through Surface.
 * <p>
 * TODO: add another bouncing rect that uses decoded video as a texture.  Useful for
 * evaluating simultaneous video playback and recording.
 * <p>
 * TODO: show the MP4 file name somewhere in the UI so people can find it in the player
 */
public class RecordFBOActivity extends AppCompatActivity implements SurfaceHolder.Callback {
    private static final String TAG = "RecordFBOActivity";

    // See the (lengthy) notes at the top of HardwareScalerActivity for thoughts about
    // Activity / Surface lifecycle management.

    private static final int RECMETHOD_DRAW_TWICE = 0;
    private static final int RECMETHOD_FBO = 1;
    private static final int RECMETHOD_BLIT_FRAMEBUFFER = 2;
    private static final int REQ_CAMERA_FPS = 30;

    private boolean mRecordingEnabled = false;          // controls button state

    private RenderThread mRenderThread;
    private static final String[] FILTERS = {
        "美颜",
        "ASCII",
        "MONEY",
        "TILE_MOSAIC",
        "FAST_BLUR",
        "BARREL_BLUR",
        "KUWAHARA",
        "Voronoi",
        "SHAKE",
        "SOUL_OUT",
        "GLITCH",
        "SCALE",
        "SHINE_WHITE"
    };
    private static final String[] VIDEO_SIZE_STR = {
        "1920x1080",
        "1280x720",
        "640x480",
        "480x360"
    };
    private static final int[][] VIDEO_SIZES = {
        {1920,1080},
        {1280,720},
        {640,480},
        {480,360}
    };
    private static final int[] BIT_RATES = {
        4000000,   // 4Mbps
        1000000,   // 1Mbps
        500000,   // 512kbps
        250000,   // 256kbps
    };
    private Spinner spinner, spinner2, spinner3;
    private SeekBar seekBar1, seekBar2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Timber.plant(new Timber.DebugTree());
        setFullScreen(this);
        setContentView(R.layout.activity_record_fbo);

        updateControls();

        SurfaceView sv = findViewById(R.id.fboActivity_surfaceView);
        sv.getHolder().addCallback(this);
        spinner = findViewById(R.id.spinner);
        spinner2 = findViewById(R.id.spinner2);
        spinner3 = findViewById(R.id.spinner3);
        seekBar1 = findViewById(R.id.seek1);
        seekBar2 = findViewById(R.id.seek2);

        Timber.d("RecordFBOActivity: onCreate done");
    }

    @Override
    protected void onPause() {
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        updateControls();
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Timber.d("surfaceCreated holder=%s", holder);

        SurfaceView sv = findViewById(R.id.fboActivity_surfaceView);
        mRenderThread = new RenderThread(getApplicationContext(), sv.getHolder(), new ActivityHandler(this),
                MiscUtils.getDisplayRefreshNsec(this));
        mRenderThread.setName("RecordFBO GL render");
        mRenderThread.start();
        mRenderThread.waitUntilReady();
        Timber.d("====main thread go on====");
        mRenderThread.setRecordMethod(RECMETHOD_FBO);

        RenderHandler rh = mRenderThread.getHandler();
        if (rh != null) {
            rh.sendSurfaceCreated();
        }
        spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, FILTERS));
        spinner3.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, VIDEO_SIZE_STR));
      spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
          Log.d(TAG, "onItemSelected: " + position);
          mRenderThread.changeFilterProgram(position);
        }

        @Override
        public void onNothingSelected(AdapterView<?> parent) {

        }
      });
      spinner3.setOnItemSelectedListener(new OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
            Log.d(TAG, "onItemSelected video: " + position);
          mRenderThread.setVideoSize(VIDEO_SIZES[position][0], VIDEO_SIZES[position][1], BIT_RATES[position]);
        }

        @Override
        public void onNothingSelected(AdapterView<?> parent) {

        }
      });
        seekBar1.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (mRenderThread.mFilterProgram instanceof BeautifyFilterProgram) {
                    float f = progress / 100f;
                    ((BeautifyFilterProgram) mRenderThread.mFilterProgram).setSmoothOpacity(f);
                } else if (mRenderThread.mFilterProgram instanceof TileMosaicFilterProgram) {
                    ((TileMosaicFilterProgram) mRenderThread.mFilterProgram).setTileSize(progress + 10);
                } else if (mRenderThread.mFilterProgram instanceof KuwaharaFilterProgram) {
                    ((KuwaharaFilterProgram) mRenderThread.mFilterProgram).setRadius(progress / 20);
                } else if (mRenderThread.mFilterProgram instanceof MosaicFilterProgram) {
                    ((MosaicFilterProgram) mRenderThread.mFilterProgram).setMosaicSize(progress + 1);
                } else if (mRenderThread.mFilterProgram instanceof ASCIIFilterProgram) {
                    ((ASCIIFilterProgram) mRenderThread.mFilterProgram).setZoom(progress / 20 + 1);
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });
        seekBar2.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (mRenderThread.mFilterProgram instanceof BeautifyFilterProgram) {
                } else if (mRenderThread.mFilterProgram instanceof TileMosaicFilterProgram) {
                    ((TileMosaicFilterProgram) mRenderThread.mFilterProgram).setBorderSize(progress);
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        Timber.tag(TAG).d("surfaceChanged fmt=" + format + " size=" + width + "x" + height +
            " holder=" + holder);
        RenderHandler rh = mRenderThread.getHandler();
        if (rh != null) {
            rh.sendSurfaceChanged(format, width, height);
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Timber.d("surfaceDestroyed holder=%s", holder);

        // We need to wait for the render thread to shut down before continuing because we
        // don't want the Surface to disappear out from under it mid-render.  The frame
        // notifications will have been stopped back in onPause(), but there might have
        // been one in progress.
        //
        // TODO: the RenderThread doesn't currently wait for the encoder / muxer to stop,
        //       so we can't use this as an indication that the .mp4 file is complete.

        RenderHandler rh = mRenderThread.getHandler();
        if (rh != null) {
            rh.sendShutdown();
            try {
                mRenderThread.join();
            } catch (InterruptedException ie) {
                // not expected
                throw new RuntimeException("join was interrupted", ie);
            }
        }
        mRenderThread = null;
        mRecordingEnabled = false;

        Timber.d("surfaceDestroyed complete");
    }

    /**
     * Updates the FPS counter.
     * <p>
     * Called periodically from the render thread (via ActivityHandler).
     */
    void handleUpdateFps(int tfps, int dropped) {
        String str = String.format(Locale.US, "FRAME:%1$.3f fps (%2$d dropped)", tfps / 1000.0f, dropped);
        TextView tv = findViewById(R.id.frameRateLabel_text);
        tv.setText(str);
    }

    void handleCameraSize(List<Camera.Size> sizes) {
        if (spinner2.getAdapter() == null) {
            String[] items = new String[sizes.size()];
            for (int i = 0; i < sizes.size(); i++) {
                items[i] = sizes.get(i).width + "x" + sizes.get(i).height;
            }
            spinner2.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items));
            spinner2.setOnItemSelectedListener(new OnItemSelectedListener() {
                @Override
                public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                    Log.d(TAG, "onItemSelected size: " + position);
                    RenderHandler rh = mRenderThread.getHandler();
                    if (rh != null) {
                        rh.sendSizeChange(position);
                    }
                }

                @Override
                public void onNothingSelected(AdapterView<?> parent) {

                }
            });
        }
    }

    /**
     * onClick handler for "record" button.
     * <p>
     * Ideally we'd grey out the button while in a state of transition, e.g. while the
     * MediaMuxer finishes creating the file, and in the (very brief) period before the
     * SurfaceView's surface is created.
     */
    public void clickToggleRecording(@SuppressWarnings("unused") View unused) {
        Log.d(TAG, "clickToggleRecording");
        RenderHandler rh = mRenderThread.getHandler();
        if (rh != null) {
            mRecordingEnabled = !mRecordingEnabled;
            updateControls();
            rh.setRecordingEnabled(mRecordingEnabled);
        }
    }

    /**
     * Updates the on-screen controls to reflect the current state of the app.
     */
    private void updateControls() {
        Button toggleRelease = findViewById(R.id.fboRecord_button);
        toggleRelease.setText(mRecordingEnabled ? "RECORDING" : "OFF");
    }


    /**
     * Handles messages sent from the render thread to the UI thread.
     * <p>
     * The object is created on the UI thread, and all handlers run there.
     */
    static class ActivityHandler extends Handler {
        private static final int MSG_UPDATE_FPS = 0;
        private static final int MSG_UPDATE_PREVIEW = 1;

        // Weak reference to the Activity; only access this from the UI thread.
        private WeakReference<RecordFBOActivity> mWeakActivity;

        public ActivityHandler(RecordFBOActivity activity) {
            mWeakActivity = new WeakReference<>(activity);
        }

        public WeakReference<RecordFBOActivity> getActivity() {
            return mWeakActivity;
        }

        /**
         * Send an FPS update.  "fps" should be in thousands of frames per second
         * (i.e. fps * 1000), so we can get fractional fps even though the Handler only
         * supports passing integers.
         * <p>
         * Call from non-UI thread.
         */
        public void sendFpsUpdate(int tfps, int dropped) {
            sendMessage(obtainMessage(MSG_UPDATE_FPS, tfps, dropped));
        }

        public void sendCameraSize(List<Camera.Size> sizes) {
            sendMessage(obtainMessage(MSG_UPDATE_PREVIEW, sizes));
        }

        @Override  // runs on UI thread
        public void handleMessage(Message msg) {
            int what = msg.what;
            //Log.d(TAG, "ActivityHandler [" + this + "]: what=" + what);

            RecordFBOActivity activity = mWeakActivity.get();
            if (activity == null) {
                Timber.tag(TAG).w("ActivityHandler.handleMessage: activity is null");
                return;
            }

            switch (what) {
                case MSG_UPDATE_FPS:
                    activity.handleUpdateFps(msg.arg1, msg.arg2);
                    break;
                case MSG_UPDATE_PREVIEW:
                    activity.handleCameraSize((List<Camera.Size>) msg.obj);
                    break;
                default:
                    throw new RuntimeException("unknown msg " + what);
            }
        }
    }


    /**
     * This class handles all OpenGL rendering.
     * <p>
     * We use Choreographer to coordinate with the device vsync.  We deliver one frame
     * per vsync.  We can't actually know when the frame we render will be drawn, but at
     * least we get a consistent frame interval.
     * <p>
     * Start the render thread after the Surface has been created.
     */
    private static class RenderThread extends Thread implements
        SurfaceTexture.OnFrameAvailableListener {
        // Object must be created on render thread to get correct Looper, but is used from
        // UI thread, so we need to declare it volatile to ensure the UI thread sees a fully
        // constructed object.
        private volatile RenderHandler mHandler;

        // Handler we can send messages to if we want to update the app UI.
        private ActivityHandler mActivityHandler;

        // Used to wait for the thread to start.
        private final Object mStartLock = new Object();
        private boolean mReady = false;

        private volatile SurfaceHolder mSurfaceHolder;  // may be updated by UI thread
        private EglCore mEglCore;
        private WindowSurface mWindowSurface;
        private FlatShadedProgram mFlatProgram;
        private TextureFilterProgram mTexProgram;

        // Orthographic projection matrix.
        private float[] mDisplayProjectionMatrix = new float[16];

        private final Drawable2d mTriDrawable = new Drawable2d(Drawable2d.Prefab.TRIANGLE);
        private final Drawable2d mRectDrawable = new Drawable2d(Drawable2d.Prefab.RECTANGLE);
        private final Drawable2d mFullDrawable = new Drawable2d(Drawable2d.Prefab.FULL_RECTANGLE);

        // One spinning triangle, one bouncing rectangle, and four edge-boxes.
        private Sprite2d mTri;
        private Sprite2d mRect;
        private Sprite2d mRecordRect;
        private float mRectVelX, mRectVelY;     // velocity, in viewport units per second
        private float mInnerLeft, mInnerTop, mInnerRight, mInnerBottom;
        private Camera mCamera;
        private Camera.Size mCameraSize;
        // Receives the output from the camera preview.
        private SurfaceTexture mCameraTexture;
        private int mCameraTextureId;
        private TextureFilterProgram mFilterProgram;
        private int programType;
        private boolean filterChanged;
        private int surfaceWidth, surfaceHeight;
        private final float[] mSTMatrix = new float[16];
        private Context context;

        // Previous frame time.
        private long mPrevTimeNanos;

        // FPS / drop counter.
        private long mRefreshPeriodNanos;
        private long mFpsCountStartNanos;
        private int mFpsCountFrame;
        private int mDroppedFrames;

        // Used for off-screen rendering.
        private int mFBOTexture;
        private int mFramebuffer;
        private int mDepthBuffer;

        // Used for recording.
        private boolean mRecordingEnabled;
        private WindowSurface mInputWindowSurface;
        private TextureMovieEncoder2 mVideoEncoder;
        private MediaMuxerWrapper mAudioMuxer;
        private int mRecordMethod;
        private boolean mRecordedPrevious;
        private Rect mVideoRect;
        private int mCameraSizeIndex;
        private int videoWidth, videoHeight;
        private int bitRate;


        /**
         * Pass in the SurfaceView's SurfaceHolder.  Note the Surface may not yet exist.
         */
        public RenderThread(Context context, SurfaceHolder holder, ActivityHandler ahandler,
                long refreshPeriodNs) {
            this.context = context;
            mSurfaceHolder = holder;
            mActivityHandler = ahandler;
            mRefreshPeriodNanos = refreshPeriodNs;

            mVideoRect = new Rect();

            mTri = new Sprite2d(mTriDrawable);
            mRect = new Sprite2d(mRectDrawable);
            mRecordRect = new Sprite2d(mRectDrawable);
        }

        /**
         * Thread entry point.
         * <p>
         * The thread should not be started until the Surface associated with the SurfaceHolder
         * has been created.  That way we don't have to wait for a separate "surface created"
         * message to arrive.
         */
        @Override
        public void run() {
            Looper.prepare();
            mHandler = new RenderHandler(this);
            mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE | EglCore.FLAG_TRY_GLES3);
            synchronized (mStartLock) {
                mReady = true;
                Log.d(TAG, "====ready====");
                mStartLock.notify();    // signal waitUntilReady()
            }

            openCamera(0);
            Looper.loop();

            Log.d(TAG, "looper quit");
            releaseCamera();
            releaseGl();
            mEglCore.release();

            synchronized (mStartLock) {
                mReady = false;
            }
        }

        /**
         * Waits until the render thread is ready to receive messages.
         * <p>
         * Call from the UI thread.
         */
        public void waitUntilReady() {
            synchronized (mStartLock) {
                while (!mReady) {
                    try {
                        mStartLock.wait();
                    } catch (InterruptedException ie) { /* not expected */ }
                }
            }
        }

        /**
         * Shuts everything down.
         */
        private void shutdown() {
            Timber.d("shutdown");
            stopEncoder();
            Looper.myLooper().quit();
        }

        /**
         * Returns the render thread's Handler.  This may be called from any thread.
         */
        public RenderHandler getHandler() {
            return mHandler;
        }

        /**
         * Prepares the surface.
         */
        private void surfaceCreated() {
            Surface surface = mSurfaceHolder.getSurface();
            prepareGl(surface);
        }

        /**
         * Prepares window surface and GL state.
         */
        private void prepareGl(Surface surface) {
            Timber.tag(TAG).d("prepareGl");

            mWindowSurface = new WindowSurface(mEglCore, surface, false);
            mWindowSurface.makeCurrent();

            mFlatProgram = new FlatShadedProgram();
            // Used for blitting texture to FBO.
            mTexProgram = new TextureFilterProgram(context, TextureFilterProgram.ProgramType.TEXTURE_2D);
            mFilterProgram = createFilterProgram();
            mCameraTextureId = mFilterProgram.createTextureObject();
            mCameraTexture = new SurfaceTexture(mCameraTextureId);
            mCameraTexture.setOnFrameAvailableListener(this);
            Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.image);
            mRect.setTexture(GlUtil.createBitmapTexture(bitmap));
            bitmap.recycle();

            // Set the background color.
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

            // Disable depth testing -- we're 2D only.
            GLES20.glDisable(GLES20.GL_DEPTH_TEST);

            // Don't need backface culling.  (If you're feeling pedantic, you can turn it on to
            // make sure we're defining our shapes correctly.)
            GLES20.glDisable(GLES20.GL_CULL_FACE);

            Timber.d("GlVersion=%d", mEglCore.getGlVersion());
        }

        public void changeFilterProgram(int type) {
            if (programType != type) {
                filterChanged = true;
                programType = type;
            }
        }

        private TextureFilterProgram createFilterProgram() {
            switch (programType) {
                case 0:
                    return new BeautifyFilterProgram(context);
                case 1:
                    return new ASCIIFilterProgram(context);
                case 2:
                    return new MoneyFilterProgram(context);
                case 3:
                    return new TileMosaicFilterProgram(context);
                case 4:
                    return new FastBlurFilterProgram(context);
                case 5:
                    return new BarrelBlurFilterProgram(context);
                case 6:
                    return new KuwaharaFilterProgram(context);
                case 7:
                    return new MosaicFilterProgram(context);
                case 8:
                    return new ShakeFilterProgram(context);
                case 9:
                    return new SoulOutFilterProgram(context);
                case 10:
                    return new GlitchFilterProgram(context);
                case 11:
                    return new ScaleFilterProgram(context);
                case 12:
                    return new ShineWhiteFilterProgram(context);
                default:
                    throw new IllegalStateException("invalid program type:" + programType);
            }
        }

        public void setVideoSize(int width, int height, int bitRate) {
            videoWidth = width;
            videoHeight = height;
            this.bitRate = bitRate;
        }

        @Override
        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
            mHandler.sendDoFrame(System.nanoTime());
        }

        /**
         * Handles changes to the size of the underlying surface.  Adjusts viewport as needed.
         * Must be called before we start drawing.
         * (Called from RenderHandler.)
         */
        private void surfaceChanged(int width, int height) {
            Timber.tag(TAG).d("surfaceChanged " + width + "x" + height);
            surfaceWidth = width;
            surfaceHeight = height;

            prepareFramebuffer(width, height);

            // Use full window.
            GLES20.glViewport(0, 0, width, height);

            // Simple orthographic projection, with (0,0) in lower-left corner.
            Matrix.orthoM(mDisplayProjectionMatrix, 0, 0, width, 0, height, -1, 1);

            int smallDim = Math.min(width, height);

            // Set initial shape size / position / velocity based on window size.  Movement
            // has the same "feel" on all devices, but the actual path will vary depending
            // on the screen proportions.  We do it here, rather than defining fixed values
            // and tweaking the projection matrix, so that our squares are square.
            mTri.setColor(0.1f, 0.9f, 0.1f);
            mTri.setScale(smallDim / 4.0f, smallDim / 4.0f);
            mTri.setPosition(width / 2.0f, height / 2.0f);
            mRect.setColor(0.9f, 0.1f, 0.1f);
            mRect.setScale(smallDim / 8.0f, smallDim / 8.0f);
            mRect.setPosition(width / 2.0f, height / 2.0f);
            mRectVelX = 1 + smallDim / 4.0f;
            mRectVelY = 1 + smallDim / 5.0f;

            // left edge
            float edgeWidth = 1 + smallDim / 64.0f;

            mRecordRect.setColor(1.0f, 1.0f, 1.0f);
            mRecordRect.setScale(edgeWidth * 2f, edgeWidth * 2f);
            mRecordRect.setPosition(edgeWidth / 2.0f, edgeWidth / 2.0f);

            // Inner bounding rect, used to bounce objects off the walls.
            mInnerLeft = mInnerBottom = 0;
            mInnerRight = width;
            mInnerTop = height;

            Timber.d("mTri: %s", mTri);
            Timber.d("mRect: %s", mRect);

            mFilterProgram.setSurfaceSize(mCameraSize.width, mCameraSize.height);
            // Ready to go, start the camera.
            startCamera();
        }

        private void sizeChanged(int position) {
            if (mCameraSizeIndex == position) {
                return;
            }
            releaseCamera();
            openCamera(position);
            startCamera();
        }

        private void startCamera() {
            Timber.d("starting camera preview");
            try {
                mCamera.setPreviewTexture(mCameraTexture);
            } catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }
            mCamera.startPreview();
        }

        private void openCamera(int position) {
            Timber.d("openCamera -- start");
            if (mCamera != null) {
                throw new RuntimeException("camera already initialized");
            }

            Camera.CameraInfo info = new Camera.CameraInfo();

            // Try to find a front-facing camera (e.g. for videoconferencing).
            int numCameras = Camera.getNumberOfCameras();
            for (int i = 0; i < numCameras; i++) {
                Camera.getCameraInfo(i, info);
                if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                    mCamera = Camera.open(i);
                    break;
                }
            }
            if (mCamera == null) {
                Log.w(TAG, "No front-facing camera found; opening default");
                Camera.getCameraInfo(CameraInfo.CAMERA_FACING_BACK, info);
                mCamera = Camera.open();    // opens first back-facing camera
            }
            if (mCamera == null) {
                throw new RuntimeException("Unable to open camera");
            }

            mCameraSizeIndex = position;
            Camera.Parameters parms = mCamera.getParameters();

            List<Camera.Size> sizes = parms.getSupportedPreviewSizes();
            mActivityHandler.sendCameraSize(sizes);
            mCameraSize = sizes.get(position);
            parms.setPreviewSize(mCameraSize.width, mCameraSize.height);

            // Try to set the frame rate to a constant value.
            int thousandFps = CameraUtils.chooseFixedPreviewFps(parms, REQ_CAMERA_FPS * 1000);

            // Give the camera a hint that we're recording video.  This can have a big
            // impact on frame rate.
            parms.setRecordingHint(true);

            mCamera.setParameters(parms);

            int orientation = 0;
            Activity activity = mActivityHandler.getActivity().get();
            if (activity != null) {
                orientation = setCameraDisplayOrientation(activity, info, mCamera);
            }

            int[] fpsRange = new int[2];
            Camera.Size mCameraPreviewSize = parms.getPreviewSize();
            parms.getPreviewFpsRange(fpsRange);
            String previewFacts = mCameraPreviewSize.width + "x" + mCameraPreviewSize.height;
            previewFacts += "[" + orientation + "]";
            previewFacts += " @[" + (fpsRange[0] / 1000.0) + " - " + (fpsRange[1] / 1000.0) + "] fps";
            Timber.i("Camera config: %s", previewFacts);
        }

        private void releaseCamera() {
            if (mCamera != null) {
                mCamera.stopPreview();
                mCamera.release();
                mCamera = null;
                Timber.d("releaseCamera -- done");
            }
        }

        /**
         * Prepares the off-screen framebuffer.
         */
        private void prepareFramebuffer(int width, int height) {
            GlUtil.checkGlError("prepareFramebuffer start");

            int[] values = new int[1];

            // Create a texture object and bind it.  This will be the color buffer.
            GLES20.glGenTextures(1, values, 0);
            GlUtil.checkGlError("glGenTextures");
            mFBOTexture = values[0];   // expected > 0
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFBOTexture);
            GlUtil.checkGlError("glBindTexture " + mFBOTexture);

            // Create texture storage.
            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
                    GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);

            // Set parameters.  We're probably using non-power-of-two dimensions, so
            // some values may not be available for use.
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
                    GLES20.GL_NEAREST);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
                    GLES20.GL_LINEAR);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
                    GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
                    GLES20.GL_CLAMP_TO_EDGE);
            GlUtil.checkGlError("glTexParameter");

            // Create framebuffer object and bind it.
            GLES20.glGenFramebuffers(1, values, 0);
            GlUtil.checkGlError("glGenFramebuffers");
            mFramebuffer = values[0];    // expected > 0
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebuffer);
            GlUtil.checkGlError("glBindFramebuffer " + mFramebuffer);

            // Create a depth buffer and bind it.
            GLES20.glGenRenderbuffers(1, values, 0);
            GlUtil.checkGlError("glGenRenderbuffers");
            mDepthBuffer = values[0];    // expected > 0
            GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mDepthBuffer);
            GlUtil.checkGlError("glBindRenderbuffer " + mDepthBuffer);

            // Allocate storage for the depth buffer.
            GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16,
                    width, height);
            GlUtil.checkGlError("glRenderbufferStorage");

            // Attach the depth buffer and the texture (color buffer) to the framebuffer object.
            GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
                    GLES20.GL_RENDERBUFFER, mDepthBuffer);
            GlUtil.checkGlError("glFramebufferRenderbuffer");
            GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
                    GLES20.GL_TEXTURE_2D, mFBOTexture, 0);
            GlUtil.checkGlError("glFramebufferTexture2D");

            // See if GLES is happy with all this.
            int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
            if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
                throw new RuntimeException("Framebuffer not complete, status=" + status);
            }

            // Switch back to the default framebuffer.
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);

            GlUtil.checkGlError("prepareFramebuffer done");
        }

        /**
         * Releases most of the GL resources we currently hold.
         * <p>
         * Does not release EglCore.
         */
        private void releaseGl() {
            GlUtil.checkGlError("releaseGl start");

            int[] values = new int[1];

            if (mWindowSurface != null) {
                mWindowSurface.release();
                mWindowSurface = null;
            }
            if (mFlatProgram != null) {
                mFlatProgram.release();
                mFlatProgram = null;
            }
            if (mFBOTexture > 0) {
                values[0] = mFBOTexture;
                GLES20.glDeleteTextures(1, values, 0);
                mFBOTexture = -1;
            }
            if (mFramebuffer > 0) {
                values[0] = mFramebuffer;
                GLES20.glDeleteFramebuffers(1, values, 0);
                mFramebuffer = -1;
            }
            if (mDepthBuffer > 0) {
                values[0] = mDepthBuffer;
                GLES20.glDeleteRenderbuffers(1, values, 0);
                mDepthBuffer = -1;
            }
            if (mTexProgram != null) {
                if (false) { // TODO: should be "true"; must ensure mEglCore current
                    mTexProgram.release();
                }
                mTexProgram = null;
            }
            if (mFilterProgram != null) {
                mFilterProgram.release();
                mFilterProgram = null;
            }

            GlUtil.checkGlError("releaseGl done");

            mEglCore.makeNothingCurrent();
        }

        /**
         * Updates the recording state.  Stops or starts recording as needed.
         */
        private void setRecordingEnabled(boolean enabled) {
            if (enabled == mRecordingEnabled) {
                return;
            }
            if (enabled) {
                startEncoder();
            } else {
                stopEncoder();
            }
            mRecordingEnabled = enabled;
        }

        /**
         * Changes the method we use to render frames to the encoder.
         */
        private void setRecordMethod(int recordMethod) {
            Timber.d("RT: setRecordMethod %d", recordMethod);
            mRecordMethod = recordMethod;
        }

        /**
         * Creates the video encoder object and starts the encoder thread.  Creates an EGL
         * surface for encoder input.
         */
        private void startEncoder() {
            Timber.d("starting to record");
            // Record at 1280x720, regardless of the window dimensions.  The encoder may
            // explode if given "strange" dimensions, e.g. a width that is not a multiple
            // of 16.  We can box it as needed to preserve dimensions.
            //videoWidth > videoHeight, eg.1280x720
            final int width = surfaceWidth < surfaceHeight ? videoHeight : videoWidth;
            final int height = surfaceWidth < surfaceHeight ? videoWidth : videoHeight;
            Timber.d("video:[%dx%d], window:[%dx%d]", width, height, surfaceWidth, surfaceHeight);
            mVideoRect.set(0, 0, width, height);

            VideoEncoderCore encoderCore;
            try {
                encoderCore = new VideoEncoderCore(width, height, bitRate, REQ_CAMERA_FPS, getCaptureFile(".mp4"));
                mAudioMuxer = new MediaMuxerWrapper(getCaptureFile(".m4a"));
                new MediaAudioEncoder(mAudioMuxer, new MediaEncoderListener() {
                    @Override
                    public void onPrepared(MediaEncoder encoder) {

                    }

                    @Override
                    public void onStopped(MediaEncoder encoder) {

                    }
                });
                mAudioMuxer.prepare();
                mAudioMuxer.startRecording();
            } catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }
            mInputWindowSurface = new WindowSurface(mEglCore, encoderCore.getInputSurface(), true);
            mVideoEncoder = new TextureMovieEncoder2(encoderCore);
        }

        /**
         * Stops the video encoder if it's running.
         */
        private void stopEncoder() {
            if (mVideoEncoder != null) {
                Log.d(TAG, "stopping recorder, mVideoEncoder=" + mVideoEncoder);
                mVideoEncoder.stopRecording();
                // TODO: wait (briefly) until it finishes shutting down so we know file is
                //       complete, or have a callback that updates the UI
                mVideoEncoder = null;
            }
            if (mAudioMuxer != null) {
                mAudioMuxer.stopRecording();
                mAudioMuxer = null;
            }
            if (mInputWindowSurface != null) {
                mInputWindowSurface.release();
                mInputWindowSurface = null;
            }
        }

        /**
         * Advance state and draw frame in response to a vsync event.
         */
        private void doFrame(long timeStampNanos) {
            // If we're not keeping up 60fps -- maybe something in the system is busy, maybe
            // recording is too expensive, maybe the CPU frequency governor thinks we're
            // not doing and wants to drop the clock frequencies -- we need to drop frames
            // to catch up.  The "timeStampNanos" value is based on the system monotonic
            // clock, as is System.nanoTime(), so we can compare the values directly.
            //
            // Our clumsy collision detection isn't sophisticated enough to deal with large
            // time gaps, but it's nearly cost-free, so we go ahead and do the computation
            // either way.
            //
            // We can reduce the overhead of recording, as well as the size of the movie,
            // by recording at ~30fps instead of the display refresh rate.  As a quick hack
            // we just record every-other frame, using a "recorded previous" flag.

            mCameraTexture.updateTexImage();
            update(timeStampNanos);

            long diff = System.nanoTime() - timeStampNanos;
            long max = mRefreshPeriodNanos - 2000000;   // if we're within 2ms, don't bother
            if (diff > max) {
                // too much, drop a frame
                Log.d(TAG, "diff is " + (diff / 1000000.0) + " ms, max " + (max / 1000000.0) +
                        ", skipping render");
                mRecordedPrevious = false;
                mDroppedFrames++;
                return;
            }

            boolean swapResult;

            if (!mRecordingEnabled || mRecordedPrevious) {
                mRecordedPrevious = false;
                // Render the scene, swap back to front.
                draw();
                swapResult = mWindowSurface.swapBuffers();
            } else {
                //mRecordedPrevious = true;

                // recording
                if (mRecordMethod == RECMETHOD_DRAW_TWICE) {
                    //Log.d(TAG, "MODE: draw 2x");

                    // Draw for display, swap.
                    draw();
                    swapResult = mWindowSurface.swapBuffers();

                    // Draw for recording, swap.
                    mVideoEncoder.frameAvailableSoon();
                    mInputWindowSurface.makeCurrent();
                    // If we don't set the scissor rect, the glClear() we use to draw the
                    // light-grey background will draw outside the viewport and muck up our
                    // letterboxing.  Might be better if we disabled the test immediately after
                    // the glClear().  Of course, if we were clearing the frame background to
                    // black it wouldn't matter.
                    //
                    // We do still need to clear the pixels outside the scissor rect, of course,
                    // or we'll get garbage at the edges of the recording.  We can either clear
                    // the whole thing and accept that there will be a lot of overdraw, or we
                    // can issue multiple scissor/clear calls.  Some GPUs may have a special
                    // optimization for zeroing out the color buffer.
                    //
                    // For now, be lazy and zero the whole thing.  At some point we need to
                    // examine the performance here.
                    GLES20.glClearColor(0f, 0f, 0f, 1f);
                    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

                    GLES20.glViewport(mVideoRect.left, mVideoRect.top,
                            mVideoRect.width(), mVideoRect.height());
                    GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
                    GLES20.glScissor(mVideoRect.left, mVideoRect.top,
                            mVideoRect.width(), mVideoRect.height());
                    draw();
                    GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
                    mInputWindowSurface.setPresentationTime(timeStampNanos);
                    mInputWindowSurface.swapBuffers();

                    // Restore.
                    GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight);
                    mWindowSurface.makeCurrent();

                } else if (mEglCore.getGlVersion() >= 3 &&
                        mRecordMethod == RECMETHOD_BLIT_FRAMEBUFFER) {
                    //Log.d(TAG, "MODE: blitFramebuffer");
                    // Draw the frame, but don't swap it yet.
                    draw();

                    mVideoEncoder.frameAvailableSoon();
                    mInputWindowSurface.makeCurrentReadFrom(mWindowSurface);
                    // Clear the pixels we're not going to overwrite with the blit.  Once again,
                    // this is excessive -- we don't need to clear the entire screen.
                    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
                    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
                    GlUtil.checkGlError("before glBlitFramebuffer");
                    Log.v(TAG, "glBlitFramebuffer: 0,0," + mWindowSurface.getWidth() + "," +
                            mWindowSurface.getHeight() + "  " + mVideoRect.left + "," +
                            mVideoRect.top + "," + mVideoRect.right + "," + mVideoRect.bottom +
                            "  COLOR_BUFFER GL_NEAREST");
                    GLES30.glBlitFramebuffer(
                            0, 0, mWindowSurface.getWidth(), mWindowSurface.getHeight(),
                            mVideoRect.left, mVideoRect.top, mVideoRect.right, mVideoRect.bottom,
                            GLES30.GL_COLOR_BUFFER_BIT, GLES30.GL_NEAREST);
                    int err;
                    if ((err = GLES30.glGetError()) != GLES30.GL_NO_ERROR) {
                        Log.w(TAG, "ERROR: glBlitFramebuffer failed: 0x" +
                                Integer.toHexString(err));
                    }
                    mInputWindowSurface.setPresentationTime(timeStampNanos);
                    mInputWindowSurface.swapBuffers();

                    // Now swap the display buffer.
                    mWindowSurface.makeCurrent();
                    swapResult = mWindowSurface.swapBuffers();

                } else {
                    //Log.d(TAG, "MODE: offscreen + blit 2x");
                    // Render offscreen.
                    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebuffer);
                    GlUtil.checkGlError("glBindFramebuffer");
                    draw();

                    // Blit to display.
                    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
                    GlUtil.checkGlError("glBindFramebuffer");
                    drawFullScreen(mTexProgram, mFBOTexture, GlUtil.IDENTITY_MATRIX);
                    swapResult = mWindowSurface.swapBuffers();

                    // Blit to encoder.
                    mVideoEncoder.frameAvailableSoon();
                    mInputWindowSurface.makeCurrent();
                    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);    // again, only really need to
                    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);     //  clear pixels outside rect
                    GLES20.glViewport(mVideoRect.left, mVideoRect.top,
                            mVideoRect.width(), mVideoRect.height());
                    drawFullScreen(mTexProgram, mFBOTexture, GlUtil.IDENTITY_MATRIX);
                    mInputWindowSurface.setPresentationTime(timeStampNanos);
                    mInputWindowSurface.swapBuffers();

                    // Restore previous values.
                    GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight);
                    mWindowSurface.makeCurrent();
                }
            }
            if (filterChanged) {
                filterChanged = false;
                mFilterProgram.release();
                mFilterProgram = createFilterProgram();
                mFilterProgram.setSurfaceSize(mCameraSize.width, mCameraSize.height);
            }

            if (!swapResult) {
                // This can happen if the Activity stops without waiting for us to halt.
                Log.w(TAG, "swapBuffers failed, killing renderer thread");
                shutdown();
                return;
            }

            // Update the FPS counter.
            //
            // Ideally we'd generate something approximate quickly to make the UI look
            // reasonable, then ease into longer sampling periods.
            final int NUM_FRAMES = 120;
            final long ONE_TRILLION = 1000000000000L;
            if (mFpsCountStartNanos == 0) {
                mFpsCountStartNanos = timeStampNanos;
                mFpsCountFrame = 0;
            } else {
                mFpsCountFrame++;
                if (mFpsCountFrame == NUM_FRAMES) {
                    // compute thousands of frames per second
                    long elapsed = timeStampNanos - mFpsCountStartNanos;
                    mActivityHandler.sendFpsUpdate((int)(NUM_FRAMES * ONE_TRILLION / elapsed),
                            mDroppedFrames);

                    // reset
                    mFpsCountStartNanos = timeStampNanos;
                    mFpsCountFrame = 0;
                }
            }
        }

        private void drawFullScreen(TextureFilterProgram program, int textureId, float[] texMatrix) {
            // Use the identity matrix for MVP so our 2x2 FULL_RECTANGLE covers the viewport.
            program.draw(GlUtil.IDENTITY_MATRIX, mFullDrawable.getVertexArray(), 0,
                mFullDrawable.getVertexCount(), mFullDrawable.getCoordsPerVertex(),
                mFullDrawable.getVertexStride(),
                texMatrix, mFullDrawable.getTexCoordArray(), textureId,
                mFullDrawable.getTexCoordStride());
        }
        /**
         * We use the time delta from the previous event to determine how far everything
         * moves.  Ideally this will yield identical animation sequences regardless of
         * the device's actual refresh rate.
         */
        private void update(long timeStampNanos) {
            // Compute time from previous frame.
            long intervalNanos;
            if (mPrevTimeNanos == 0) {
                intervalNanos = 0;
            } else {
                intervalNanos = timeStampNanos - mPrevTimeNanos;

                final long ONE_SECOND_NANOS = 1000000000L;
                if (intervalNanos > ONE_SECOND_NANOS) {
                    // A gap this big should only happen if something paused us.  We can
                    // either cap the delta at one second, or just pretend like this is
                    // the first frame and not advance at all.
                    Log.d(TAG, "Time delta too large: " +
                            (double) intervalNanos / ONE_SECOND_NANOS + " sec");
                    intervalNanos = 0;
                }
            }
            mPrevTimeNanos = timeStampNanos;

            final float ONE_BILLION_F = 1000000000.0f;
            final float elapsedSeconds = intervalNanos / ONE_BILLION_F;

            // Spin the triangle.  We want one full 360-degree rotation every 3 seconds,
            // or 120 degrees per second.
            final int SECS_PER_SPIN = 3;
            float angleDelta = (360.0f / SECS_PER_SPIN) * elapsedSeconds;
            mTri.setRotation(mTri.getRotation() + angleDelta);

            // Bounce the rect around the screen.  The rect is a 1x1 square scaled up to NxN.
            // We don't do fancy collision detection, so it's possible for the box to slightly
            // overlap the edges.  We draw the edges last, so it's not noticeable.
            float xpos = mRect.getPositionX();
            float ypos = mRect.getPositionY();
            float xscale = mRect.getScaleX();
            float yscale = mRect.getScaleY();
            xpos += mRectVelX * elapsedSeconds;
            ypos += mRectVelY * elapsedSeconds;
            if ((mRectVelX < 0 && xpos - xscale/2 < mInnerLeft) ||
                    (mRectVelX > 0 && xpos + xscale/2 > mInnerRight)) {
                mRectVelX = -mRectVelX;
            }
            if ((mRectVelY < 0 && ypos - yscale/2 < mInnerBottom) ||
                    (mRectVelY > 0 && ypos + yscale/2 > mInnerTop)) {
                mRectVelY = -mRectVelY;
            }
            mRect.setPosition(xpos, ypos);
        }

        /**
         * Draws the scene.
         */
        private void draw() {
            GlUtil.checkGlError("draw start");

            // Clear to a non-black color to make the content easily differentiable from
            // the pillar-/letter-boxing.
            GLES20.glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            mCameraTexture.getTransformMatrix(mSTMatrix);
            drawFullScreen(mFilterProgram, mCameraTextureId, mSTMatrix);
            mTri.draw(mFlatProgram, mDisplayProjectionMatrix);
            mRect.drawTexture(mTexProgram, mDisplayProjectionMatrix);

            // Give a visual indication of the recording method.
            switch (mRecordMethod) {
                case RECMETHOD_DRAW_TWICE:
                    mRecordRect.setColor(1.0f, 0.0f, 0.0f);
                    break;
                case RECMETHOD_FBO:
                    mRecordRect.setColor(0.0f, 1.0f, 0.0f);
                    break;
                case RECMETHOD_BLIT_FRAMEBUFFER:
                    mRecordRect.setColor(0.0f, 0.0f, 1.0f);
                    break;
                default:
            }
            if (!mRecordingEnabled || (mPrevTimeNanos / 1000000000L) % 2 == 0) {
                mRecordRect.draw(mFlatProgram, mDisplayProjectionMatrix);
            }

            GlUtil.checkGlError("draw done");
        }
    }

    /**
     * Handler for RenderThread.  Used for messages sent from the UI thread to the render thread.
     * <p>
     * The object is created on the render thread, and the various "send" methods are called
     * from the UI thread.
     */
    private static class RenderHandler extends Handler {
        private static final int MSG_SURFACE_CREATED = 0;
        private static final int MSG_SURFACE_CHANGED = 1;
        private static final int MSG_DO_FRAME = 2;
        private static final int MSG_RECORDING_ENABLED = 3;
        private static final int MSG_RECORD_METHOD = 4;
        private static final int MSG_SHUTDOWN = 5;
        private static final int MSG_SIZE_CHANGE = 6;

        // This shouldn't need to be a weak ref, since we'll go away when the Looper quits,
        // but no real harm in it.
        private WeakReference<RenderThread> mWeakRenderThread;

        /**
         * Call from render thread.
         */
        public RenderHandler(RenderThread rt) {
            mWeakRenderThread = new WeakReference<>(rt);
        }

        /**
         * Sends the "surface created" message.
         * <p>
         * Call from UI thread.
         */
        public void sendSurfaceCreated() {
            sendMessage(obtainMessage(RenderHandler.MSG_SURFACE_CREATED));
        }

        /**
         * Sends the "surface changed" message, forwarding what we got from the SurfaceHolder.
         * <p>
         * Call from UI thread.
         */
        public void sendSurfaceChanged(@SuppressWarnings("unused") int format, int width, int height) {
            // ignore format
            sendMessage(obtainMessage(RenderHandler.MSG_SURFACE_CHANGED, width, height));
        }

        /**
         * Sends the "do frame" message, forwarding the Choreographer event.
         * <p>
         * Call from UI thread.
         */
        public void sendDoFrame(long frameTimeNanos) {
            sendMessage(obtainMessage(RenderHandler.MSG_DO_FRAME,
                    (int) (frameTimeNanos >> 32), (int) frameTimeNanos));
        }

        /**
         * Enable or disable recording.
         * <p>
         * Call from non-UI thread.
         */
        public void setRecordingEnabled(boolean enabled) {
            sendMessage(obtainMessage(MSG_RECORDING_ENABLED, enabled ? 1 : 0, 0));
        }

        /**
         * Set the method used to render a frame for the encoder.
         * <p>
         * Call from non-UI thread.
         */
        public void setRecordMethod(int recordMethod) {
            sendMessage(obtainMessage(MSG_RECORD_METHOD, recordMethod, 0));
        }

        /**
         * Sends the "shutdown" message, which tells the render thread to halt.
         * <p>
         * Call from UI thread.
         */
        public void sendShutdown() {
            sendMessage(obtainMessage(RenderHandler.MSG_SHUTDOWN));
        }

        public void sendSizeChange(int position) {
            sendMessage(obtainMessage(MSG_SIZE_CHANGE, position, 0));
        }

        @Override  // runs on RenderThread
        public void handleMessage(Message msg) {
            int what = msg.what;
            //Log.d(TAG, "RenderHandler [" + this + "]: what=" + what);

            RenderThread renderThread = mWeakRenderThread.get();
            if (renderThread == null) {
                Log.w(TAG, "RenderHandler.handleMessage: weak ref is null");
                return;
            }

            switch (what) {
                case MSG_SURFACE_CREATED:
                    renderThread.surfaceCreated();
                    break;
                case MSG_SURFACE_CHANGED:
                    renderThread.surfaceChanged(msg.arg1, msg.arg2);
                    break;
                case MSG_DO_FRAME:
                    long timestamp = (((long) msg.arg1) << 32) |
                                     (((long) msg.arg2) & 0xffffffffL);
                    renderThread.doFrame(timestamp);
                    break;
                case MSG_RECORDING_ENABLED:
                    renderThread.setRecordingEnabled(msg.arg1 != 0);
                    break;
                case MSG_RECORD_METHOD:
                    renderThread.setRecordMethod(msg.arg1);
                    break;
                case MSG_SHUTDOWN:
                    renderThread.shutdown();
                    break;
                case MSG_SIZE_CHANGE:
                    renderThread.sizeChanged(msg.arg1);
                    break;
               default:
                    throw new RuntimeException("unknown message " + what);
            }
        }
    }

    public static int setCameraDisplayOrientation(Activity activity, CameraInfo info, Camera camera) {
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0: degrees = 0; break;
            case Surface.ROTATION_90: degrees = 90; break;
            case Surface.ROTATION_180: degrees = 180; break;
            case Surface.ROTATION_270: degrees = 270; break;
        }

        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror
        } else {  // back-facing
            result = (info.orientation - degrees + 360) % 360;
        }
        camera.setDisplayOrientation(result);
        return result;
    }

    /**
     * generate output file
     * @param ext .mp4(.m4a for audio) or .png
     * @return return null when this app has no writing permission to external storage.
     */
    public static File getCaptureFile(final String ext) {
        final File dir = Environment.getExternalStorageDirectory();
        if (dir.canWrite()) {
            return new File(dir, "fbo-gl-recording" + ext);
        }
        return null;
    }

    public static void setFullScreen(Activity activity) {
        Window window = activity.getWindow();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
                | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(Color.TRANSPARENT);
            window.setNavigationBarColor(Color.TRANSPARENT);
        }else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
                | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
        }
    }
}
